JS 基础知识点

1、js 中存在 6 种原始类型

boolean,null,undefined,number,string,symbol;typeof null 会输出 object,但是这只是 JS 存在的一个悠久 Bug

2、判断类型

typeof 对于原始类型来说,除了 null 都可以显示正确的类型;typeof 对于对象来说,除了函数都会显示 object,所以说 typeof 并不能准确判断变量到底是什么类型。最通用的办法是运用Object.prototype.toString.call()

3、关于条件判断

在条件判断时,除了 undefined, null, false, NaN, ‘’, 0, -0,其他所有值都转为 true,包括所有对象

4、关于this指向

如果用一句话说明 this 的指向,那么即是: 谁调用它,this 就指向谁。

但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:

this 的指向可以按照以下顺序判断:

全局环境中的 this

浏览器环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 window;

node 环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部),this 都是空对象 {};

是否是 new 绑定

如果是 new 绑定,并且构造函数中没有返回 function 或者是 object,那么 this 指向这个新对象。如下:

  • 构造函数返回值不是 function 或 object。new Super() 返回的是 this 对象。
  • 构造函数返回值是 function 或 object,new Super()是返回的是Super种返回的对象。
1
2
3
4
5
6
function Super(age){
this.age = age
}

let instance = new Super('26')
console.log(instance.age) // 26
1
2
3
4
5
6
7
8
9
function Super(age){
this.age = age
let obj = {a:'2'}
return obj
}

let instance = new Super('26')
console.log(instance) // {a:'2'}
console.log(instance.age) // undefined

函数是否通过 call,apply 调用,或者使用了 bind 绑定,如果是,那么this绑定的就是指定的对象【归结为显式绑定】。

1
2
3
4
5
6
7
8
9
10
11
12
function info(){
console.log(this.age)
}
var person= {
age: 20,
info
}
var age = 28
var info = person.info
info.call(person) // 20
info.apply(person) // 20
info.bind(person)() // 20

这里同样需要注意一种特殊情况,如果 call,apply 或者 bind 传入的第一个参数值是 undefined 或者 null,严格模式下 this 的值为传入的值 null /undefined。非严格模式下,实际应用的默认绑定规则,this 指向全局对象(node环境为global,浏览器环境为window)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function info () {
// node 环境中:非严格模式 globao,严格模式 null
// 浏览器环境中:非严格模式 window, 严格模式为 null
console.log(this)
console.log(this.age)
}
var person = {
age: 20,
info
}
var age = 28
var info = person.info
// 严格模式抛出错误
// 非严格模式,node 下输出 undefined(因为全局的 age 不会挂在 global 上)
// 非严格模式,浏览器环境下输出28(因为全局的 age 回挂在 window 上)
info.call(null)

注意:不管我们连续给函数 bind 几次,fn 中的 this 永远由第一次 bind 决定:fn.bind().bind(a)() // this 指向fn

隐式绑定,函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的隐式调用为: xxx.fn()

1
2
3
4
5
6
7
8
9
function info(){
console.log(this.age)
}
var person= {
age: 20,
info
}
var age = 28
person.info() // 20,执行的是隐式绑定

默认绑定,在不能应用其它绑定规则时使用的默认规则,通常是独立函数调用。

非严格模式: node环境,指向全局对象 global,浏览器环境,指向全局对象 window。

严格模式:执行 undefined

1
2
3
4
5
6
7
8
9
function info(){
console.log(this.age)
}

var age = 28
// 严格模式浏览器环境和node环境都抛错
// 非严格模式:node下输出 undefined(因为全局的 age 不会挂在 global 上)
// 非严格模式:浏览器环境下输出 28(因为全局的 age 回挂在 window 上)
info()

箭头函数的情况

箭头函数没有自己的this,继承外层上下文绑定的this。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let obj = {
age: 20,
info: function() {
return () => {
console.log(this.age)
}
}
}
let person = {age: 28}
let info = obj.info()
info() // 20

let info2 = obj.info.call(person)
info2() // 28

5、== 和 ===

== 和 === 的区别是:对于 == 来说,如果对比双方的类型不一样的话,就会进行类型转换

6、闭包

闭包的定义:函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包

7、浅拷贝

浅拷贝对象类型在赋值的过程中其实是复制了地址,从而会导致改变了一方其他也都被改变的情况;首先可以通过 Object.assign 来解决这个问题,很多人认为这个函数是用来深拷贝的。其实并不是,Object.assign 只会拷贝所有的属性值到新的对象中,如果属性值是对象的话,拷贝的是地址,所以并不是深拷贝。

8、深拷贝

简单的深拷贝可以通过: JSON.parse(JSON.stringify(object)) 来解决,该方法的局限性在:

  1. 会忽略 undefined
  2. 会忽略 symbol
  3. 不能序列化函数
  4. 不能解决循环引用的对象

如果你所需拷贝的对象含有内置类型并且不包含函数,可以使用 MessageChannel,完全的深拷贝只能是采用递归遍历了

9、instanceof

关于 instanceof 其实表示的就是一种继承关系,或者原型链的结构。Instanceof 运算符的第一个变量是一个对象,暂时称为A;第二个变量一般是一个函数,暂时称为 B。Instanceof的判断规则是:沿着 A 的 proto 这条线来找,同时沿着 B 的 prototype 这条线来找,如果两条线能找到同一个引用,即同一个对象,那么就返回 true。如果找到终点还未重合,则返回 false。

10、关于原型,需要记住这张图

原型

11、 var、let、const的区别

  1. 函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部
  2. var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用
  3. var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会
  4. let 和 const 作用基本一致,但是后者声明的变量不能再次赋值

12、组合继承

核心是在子类的构造函数中通过 Parent.call(this) 继承父类的属性,然后改变子类的原型为 new Parent() 来继承父类的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}
function Child(value) {
Parent.call(this, value)
}
Child.prototype = new Parent()

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

这种继承方式优点在于构造函数可以传参,不会与父类引用属性共享,可以复用父类的函数,但是也存在一个缺点就是在继承父类函数的时候调用了父类构造函数,导致子类的原型上多了不需要的父类属性,存在内存上的浪费。

13、寄生组合继承

这种继承方式对组合继承进行了优化,组合继承缺点在于继承父类函数时调用了构造函数,我们只需要优化掉这点就行了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function Parent(value) {
this.val = value
}
Parent.prototype.getValue = function() {
console.log(this.val)
}

function Child(value) {
Parent.call(this, value)
}
Child.prototype = Object.create(Parent.prototype, {
constructor: {
value: Child,
enumerable: false,
writable: true,
configurable: true
}
})

const child = new Child(1)

child.getValue() // 1
child instanceof Parent // true

以上继承实现的核心就是将父类的原型赋值给了子类,并且将构造函数设置为子类,这样既解决了无用的父类属性问题,还能正确的找到子类的构造函数。

14、Class 继承

以上两种继承方式都是通过原型去解决的,在 ES6 中,我们可以使用 class 去实现继承:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Parent {
constructor(value) {
this.val = value
}
getValue() {
console.log(this.val)
}
}
class Child extends Parent {
constructor(value) {
super(value)
this.val = value
}
}
let child = new Child(1)
child.getValue() // 1
child instanceof Parent // true

class 实现继承的核心在于使用 extends 表明继承自哪个父类,并且在子类构造函数中必须调用 super,因为这段代码可以看成 Parent.call(this, value)。不过,之前也说了在 JS 中并不存在类,所以 class 的本质就是函数。

15、模块化

  1. 模块化带来的好处:解决命名冲突;提供复用性;提高代码可维护性
  2. 在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污染全局作用域的问题
  3. AMD 和 CMD
  4. CommonJS:CommonJS 最早是 Node 在使用,目前也仍然广泛使用,比如在 Webpack 中你就能见到它,当然目前在 Node 中的模块管理已经和 CommonJS 有一些区别了
  5. ES Module

16、Proxy

在 Vue3.0 中将会通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式。 Proxy 是 ES6 中新增的功能,它可以用来自定义对象中的操作。如果需要实现一个 Vue 中的响应式,需要我们在 get 中收集依赖,在 set 派发更新,之所以 Vue3.0 要使用 Proxy 替换原本的 API 原因在于 Proxy 无需一层层递归为每个属性添加代理,一次即可完成以上操作,性能上更好,并且原本的实现有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,唯一缺陷可能就是浏览器的兼容性不好了。

17、事件冒泡与捕获

浏览器里面的事件都会按照一定的规则去传递,不管 body 上绑定事件、或者 div 甚至 div 的 text 节点上绑定事件,这个事件必须先从根节点开始遍历(即Window对象开始),从上往下,传递的过程中,发现有的元素绑定了事件,也先放着,等全部事件捕获完毕(遍历完毕), 开始处理事件,处理的顺序为,从最小的根节点上的事件开始,依次向上冒泡,如图:

浏览器事件传递规则

一句话概括这种机制:

  • 捕获:自外而内,从根到叶,从大到小
  • 冒泡:自内而外,从叶到根,从小到大

还需要注意的是,并不是所有的事件都会冒泡,以下事件就没有:

  • onblur
  • onfocus
  • onmouseenter
  • onmouseleave

18、关于事件代理(事件委托)

事件代理是通过监听一个父元素,来给不同的子元素绑定事件,减少监听次数,从而提升浏览器速度。需要注意的是:如果元素被阻止冒泡了,千万别去用事件委托的方式监听事件,因为事件委托的原理是利用事件冒泡,当冒泡被阻止,就无法监听了。